/*
 * $Id: QEADIFLocation.m 73 2009-06-19 01:50:56Z jon@bway.net $
 * Chalk
 *
 * Created 5/21/09
 * Copyright Jon Gordon
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#import "QEADIFLocation.h"

#include <wctype.h>

#import "QEADIFInternal.h"

static const NSRange ewnsRange  = {0, 1};   // for ADIF Location type
static const NSRange degRange   = {1, 3};
static const NSRange minRange   = {5, 6};

#define MIN_DEGREES 0
#define MAX_DEGREES 180
#define MIN_MM 0
#define MAX_MM 59999
#define MINUTES_TO_MM_SCALE_FACTOR 1000
#define MM_TO_MINUTES_EXPONENT (-3)
#define MIN_MINUTES @"0"
#define MAX_MINUTES @"59.999"

#define DIRECTION_CHARS @"nNeEwWsS"

static NSNumber *minDegrees;         // of latitude or longitude
static NSNumber *maxDegrees;
static NSDecimalNumber *minMinutes;
static NSDecimalNumber *maxMinutes;

static NSCharacterSet *directionCharSet;

static NSPredicate *adifLocationPred;

@interface QEADIFLocation (private)
- (id) initWithDirection:(unichar)newDirection degrees:(int)newDegrees milliminutes:(int)mm;
- (id) initWithUncheckedStringValue:(NSString *)newValue
                          direction:(unichar)newDirection
                            degrees:(int)newDegrees
                       milliminutes:(int)mm;
@end


@implementation QEADIFLocation

@synthesize stringValue, direction, degrees, minutes;

#pragma mark LIFECYCLE METHODS
+ (BOOL)accessInstanceVariablesDirectly
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return NO;
} // accessInstanceVariablesDirectly

+ (void)initialize
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    if (!initialized) {
        //-NSLog(@"  initializing");
        initialized = YES;
        
        adifLocationPred  = [[NSPredicate predicateWithFormat:@"SELF matches %@",
                              ADIF_LOCATION_REGEX] retain];
        
        maxDegrees = [[NSNumber alloc] initWithInt:MIN_DEGREES];
        minDegrees = [[NSNumber alloc] initWithInt:MAX_DEGREES];
        minMinutes = [[NSDecimalNumber alloc] initWithString:MIN_MINUTES];
        maxMinutes = [[NSDecimalNumber alloc] initWithString:MAX_MINUTES];
        
        directionCharSet = [[NSCharacterSet characterSetWithCharactersInString:DIRECTION_CHARS] retain];
    } // if
} // initialize


// result of this constructor is undefined and basically useless
- (id)init
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [self initWithString:nil];
} // init

- (id)initWithString:(NSString *)locationSrc
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSString *newValue = [[locationSrc copy] autorelease];
    unichar newDirection = 0;
    int newDegrees = 0;
    int mm = 0;
    if ([QEADIFLocation isValidLocationString:&newValue]) {
        //        //-NSLog(@"  source \"%@\" is legal", locationSrc);
        // extract values for main initializer
        newDirection = [newValue characterAtIndex:0];
        newDegrees = [[newValue substringWithRange:degRange] integerValue];
        mm = [[[NSDecimalNumber decimalNumberWithString:[newValue substringWithRange:minRange]] 
               decimalNumberByMultiplyingByPowerOf10:3] 
              integerValue];
    } else {
        //        //-NSLog(@"  source \"%@\" is illegal", locationSrc);
        // send useless values to main initializer
        newValue = nil;
    } // else
    
    return [self initWithUncheckedStringValue:newValue
                                    direction:newDirection
                                      degrees:newDegrees
                                 milliminutes:mm];
} // initWithString:

- (id)initWithDirection:(unichar)newDirection degrees:(int)newDegrees milliminutes:(int)mm
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    id result = nil;
    if ([directionCharSet characterIsMember:newDirection] &&
        (MIN_DEGREES <= newDegrees) && 
        (((MAX_DEGREES > newDegrees) && (MIN_MM <= mm) && (MAX_MM >= mm)) ||
         ((MAX_DEGREES == newDegrees) && (MIN_MM == mm)))) {
        int wholePart = mm / MINUTES_TO_MM_SCALE_FACTOR;
        int fractionPart = mm - (wholePart * MINUTES_TO_MM_SCALE_FACTOR);
        NSString *newValue = [[NSString stringWithFormat:@"%C%03d %02d.%03d",
                               newDirection, newDegrees, wholePart, fractionPart] retain];
        result = [self initWithUncheckedStringValue:newValue 
                                          direction:newDirection 
                                            degrees:newDegrees 
                                       milliminutes:mm];
    } else {
        // undefined result b/c parameters are invalid
        result = [self init];
    } // else
    return result;
} // initWithDirection:degrees:milliminutes:

- (id)initWithDirection:(unichar)newDirection degrees:(NSNumber *)newDegrees minutes:(NSDecimalNumber *)newMinutes
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    id result;
    if ((nil == newDegrees) || (nil == newMinutes)) {
        result = [self init];
    } else {
        int mm = [[newMinutes decimalNumberByMultiplyingByPowerOf10:3] integerValue];
        result = [self initWithDirection:newDirection
                                 degrees:[newDegrees integerValue]
                            milliminutes:mm];
    } // else
    return result;
} // initWithDirection:degrees:minutes:

- (id)initWithScalarsDirection:(unichar)newDirection degrees:(int)newDegrees minutes:(double)newMinutes
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [self initWithDirection:newDirection 
                           degrees:newDegrees
                      milliminutes:(int)(newMinutes * (double) MINUTES_TO_MM_SCALE_FACTOR)];
} // initWithScalarDirection:degrees:minutes:

/*
 * Main initializer, which just assigns its parameters to the ivars.
 * The computation is minimal.
 */
- (id) initWithUncheckedStringValue:(NSString *)newValue
                          direction:(unichar)newDirection
                            degrees:(int)newDegrees
                       milliminutes:(int)mm
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    self = [super init];
    if (nil != self) {
        stringValue = [newValue copy];
        direction = newDirection;
        degrees = [[NSNumber alloc] initWithInt:newDegrees];
        milliminutes = mm;
        minutes = [[NSDecimalNumber alloc] initWithMantissa:mm exponent:MM_TO_MINUTES_EXPONENT isNegative:NO];
    } // if
    return self;
} // initWithUncheckedStringValue:direction:degrees:milliminutes:


- (id)copyWithZone:(NSZone *)zone {
    return [self retain];
} // copyWithZone:


- (void)dealloc
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    id tmp = [self stringValue];
    stringValue = nil;
    [tmp release];
    tmp = [self degrees];
    degrees = nil;
    [degrees release];
    tmp = [self minutes];
    minutes = nil;
    [minutes release];
    
    [super dealloc];
} // dealloc

#pragma mark OPERATIONS
+ (BOOL)isValidLocationString:(NSString **)src
{
    return [QEADIFLocation isValidLocationString:src error:nil];
} // isValidLocationString:

+ (BOOL)isValidLocationString:(NSString **)src error:(NSError **)outError
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if ((nil == src) || (nil== *src)) {
        if (nil != outError) {
            NSString *eMsg = NSLocalizedString(@"no data provided",
                                               @"");
            NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
            *outError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:QEADIFErrorDataNull
                                        userInfo:eDict];
        } // if
        return NO;
    } // if
    
    NSString *uprSrc = [[*src stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] uppercaseString];
    if (![adifLocationPred evaluateWithObject:uprSrc]) {
        if (nil != outError) {
            NSString *format = NSLocalizedString(@"\"%@\" is not a valid ADIF Location",
                                                 @"");
            NSString *eMsg = [NSString stringWithFormat:format, uprSrc];
            NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
            *outError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:QEADIFErrorDataInvalid
                                        userInfo:eDict];
        } // if 
        return NO;
    } // if
    
    NSString *degSrc = [uprSrc substringWithRange:degRange];
    NSInteger degrees = [degSrc integerValue];
    if ((MIN_DEGREES > degrees) || (degrees > MAX_DEGREES)) {
        if (nil != outError) {
            NSString *format = NSLocalizedString(@"\"%@\" is not a valid ADIF Location: degrees must be between 0 and 180",
                                                 @"");
            NSString *eMsg = [NSString stringWithFormat:format, uprSrc];
            NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
            *outError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:QEADIFErrorDataRange
                                        userInfo:eDict];
        } // if 
        return NO;
    } // if
    
    NSDecimalNumber *minutes = [NSDecimalNumber decimalNumberWithString:[uprSrc substringWithRange:minRange]];
    if (degrees == MAX_DEGREES) {
        /*
         * There seems to me to be a bug in the ADIF spec.  Taken literally,
         * version 2.2.3 of the spec suggests that 180 degrees, 59.999 minutes,
         * is a legal location.  But it probably shouldn't be.  So
         * if a location is 180 degrees, minutes has to be zero, or I declare
         * it invalid.  Amen and hallelujah.
         */
        if ([minutes compare:minMinutes] != NSOrderedSame) {
            if (nil != outError) {
                NSString *format = NSLocalizedString(@"\"%@\" is not a valid ADIF Location: when degrees equals 180, minutes must be 0",
                                                     @"");
                NSString *eMsg = [NSString stringWithFormat:format, uprSrc];
                NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
                *outError = [NSError errorWithDomain:QEADIFErrorDomain
                                                code:QEADIFErrorDataRange
                                            userInfo:eDict];
            } // if 
            return NO;
        } // if
    } else if (([minutes compare:minMinutes] == NSOrderedAscending) ||
               ([minutes compare:maxMinutes] == NSOrderedDescending)) {
        if (nil != outError) {
            NSString *format = NSLocalizedString(@"\"%@\" is not a valid ADIF Location: minutes must be between 0 and 59.999",
                                                 @"");
            NSString *eMsg = [NSString stringWithFormat:format, uprSrc];
            NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
            *outError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:QEADIFErrorDataRange
                                        userInfo:eDict];
        } // if 
        return NO;
    } // if
    
    if (![*src isEqual:uprSrc]) {
        *src = uprSrc;
    } // if
    return YES;    
} // isValidLocationString:error:


#pragma mark PROPERTIES
- (BOOL)isValid
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return (nil != [self stringValue]);
} // isValid

- (NSString *)description
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [NSString stringWithFormat:@"{location: dir=\'%C\'; deg=%@; min=%@}",
            direction, degrees, minutes];
} // description

- (BOOL)isEqual:(id)value
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == value) {
        return NO;
    } // if
    
    return (([self direction] == [value direction]) && 
            [[self degrees] isEqual:[value degrees]] &&
            [[self minutes] isEqual:[value minutes]]);
} // isEqual:
@end
